/*
 * Copyright 2002-2021 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.security.oauth2.client.web;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider;
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.UriComponentsBuilder;

/**
 * An implementation of an {@link AbstractAuthenticationProcessingFilter} for OAuth 2.0
 * Login.
 *
 * <p>
 * This authentication {@code Filter} handles the processing of an OAuth 2.0 Authorization
 * Response for the authorization code grant flow and delegates an
 * {@link OAuth2LoginAuthenticationToken} to the {@link AuthenticationManager} to log in
 * the End-User.
 * 此身份验证过滤器处理OAuth 2.0授权响应的授权码授予流，并将OAuth2LoginAuthenticationToken委托给AuthenticationManager以登录终端用户
 * <p>
 * The OAuth 2.0 Authorization Response is processed as follows:
 * OAuth 2.0授权响应的处理如下：
 * <ul>
 * <li>Assuming the End-User (Resource Owner) has granted access to the Client, the
 * Authorization Server will append the {@link OAuth2ParameterNames#CODE code} and
 * {@link OAuth2ParameterNames#STATE state} parameters to the
 * {@link OAuth2ParameterNames#REDIRECT_URI redirect_uri} (provided in the Authorization
 * Request) and redirect the End-User's user-agent back to this {@code Filter} (the
 * Client).</li>
 * 1:假设终端用户(资源所有者)已授予对客户端的访问权限，授权服务器将把code和state参数附加到redirect_uri(在授权请求中提供)，并将终端用户的用户代理重定向回此过滤器(客户端)。
 * <li>This {@code Filter} will then create an {@link OAuth2LoginAuthenticationToken} with
 * the {@link OAuth2ParameterNames#CODE code} received and delegate it to the
 * {@link AuthenticationManager} to authenticate.</li>
 * 2:然后，这个Filter将使用接收到的code创建一个OAuth2LoginAuthenticationToken，并将其委托给AuthenticationManager进行身份验证
 * <li>Upon a successful authentication, an {@link OAuth2AuthenticationToken} is created
 * (representing the End-User {@code Principal}) and associated to the
 * {@link OAuth2AuthorizedClient Authorized Client} using the
 * {@link OAuth2AuthorizedClientRepository}.</li>
 * 3:认证成功后，将创建OAuth2AuthenticationToken(代表终端用户主体)，并使用OAuth2AuthorizedClientRepository将其关联到授权客户端
 * <li>Finally, the {@link OAuth2AuthenticationToken} is returned and ultimately stored in
 * the {@link SecurityContextRepository} to complete the authentication processing.</li>
 * 4:最后，返回OAuth2AuthenticationToken并最终存储在SecurityContextRepository中，以完成身份验证处理
 * </ul>
 *
 * @author Joe Grandja
 * @since 5.0
 * @see AbstractAuthenticationProcessingFilter
 * @see OAuth2LoginAuthenticationToken
 * @see OAuth2AuthenticationToken
 * @see OAuth2LoginAuthenticationProvider
 * @see OAuth2AuthorizationRequest
 * @see OAuth2AuthorizationResponse
 * @see AuthorizationRequestRepository
 * @see OAuth2AuthorizationRequestRedirectFilter
 * @see ClientRegistrationRepository
 * @see OAuth2AuthorizedClient
 * @see OAuth2AuthorizedClientRepository
 * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section
 * 4.1 Authorization Code Grant</a>
 * @see <a target="_blank" href=
 * "https://tools.ietf.org/html/rfc6749#section-4.1.2">Section 4.1.2 Authorization
 * Response</a>
 * OAuth2.0登录认证过滤器。注：OAuth2LoginAuthenticationFilter需要添加spring-security-oauth2等相关模块
 */
public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

	/**
	 * The default {@code URI} where this {@code Filter} processes authentication
	 * requests.
	 */
	// 默认匹配格式
	public static final String DEFAULT_FILTER_PROCESSES_URI = "/login/oauth2/code/*";

	private static final String AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE = "authorization_request_not_found";

	private static final String CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE = "client_registration_not_found";

	private ClientRegistrationRepository clientRegistrationRepository;

	private OAuth2AuthorizedClientRepository authorizedClientRepository;

	private AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository();

	private Converter<OAuth2LoginAuthenticationToken, OAuth2AuthenticationToken> authenticationResultConverter = this::createAuthenticationResult;

	/**
	 * Constructs an {@code OAuth2LoginAuthenticationFilter} using the provided
	 * parameters.
	 * @param clientRegistrationRepository the repository of client registrations
	 * @param authorizedClientService the authorized client service
	 */
	public OAuth2LoginAuthenticationFilter(ClientRegistrationRepository clientRegistrationRepository,
			OAuth2AuthorizedClientService authorizedClientService) {
		this(clientRegistrationRepository, authorizedClientService, DEFAULT_FILTER_PROCESSES_URI);
	}

	/**
	 * Constructs an {@code OAuth2LoginAuthenticationFilter} using the provided
	 * parameters.
	 * @param clientRegistrationRepository the repository of client registrations
	 * @param authorizedClientService the authorized client service
	 * @param filterProcessesUrl the {@code URI} where this {@code Filter} will process
	 * the authentication requests
	 */
	public OAuth2LoginAuthenticationFilter(ClientRegistrationRepository clientRegistrationRepository,
			OAuth2AuthorizedClientService authorizedClientService, String filterProcessesUrl) {
		this(clientRegistrationRepository,
				new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService),
				filterProcessesUrl);
	}

	/**
	 * Constructs an {@code OAuth2LoginAuthenticationFilter} using the provided
	 * parameters.
	 * @param clientRegistrationRepository the repository of client registrations
	 * @param authorizedClientRepository the authorized client repository
	 * @param filterProcessesUrl the {@code URI} where this {@code Filter} will process
	 * the authentication requests
	 * @since 5.1
	 */
	public OAuth2LoginAuthenticationFilter(ClientRegistrationRepository clientRegistrationRepository,
			OAuth2AuthorizedClientRepository authorizedClientRepository, String filterProcessesUrl) {
		super(filterProcessesUrl);
		Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
		Assert.notNull(authorizedClientRepository, "authorizedClientRepository cannot be null");
		this.clientRegistrationRepository = clientRegistrationRepository;
		this.authorizedClientRepository = authorizedClientRepository;
	}
    // 当Spring Security拦截到回调接口后会封装一个OAuth2LoginAuthenticationToken交给AuthenticationManager进行认证
	// 即下面的(OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest)
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		// 解析获取request中code等参数值
		MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
		// 判断携带的参数是否符合OAuth2.0规范，不是则抛异常
		// 规则：成功时必须包含state和code键值，失败时state和error键值，否则就不是OAuth的请求
		if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
			//抛出异常:该请求缺少必需的参数、包含无效的参数值、多次包含参数或格式不正确
			OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}
		// 这里通过传回来的state的属性跟session中存储的OAuth2AuthorizationRequest对象中的state属性进行比对，以防止CSRF攻击--比对成功后清除session中的state
		// 这个state属性在之前OAuth2AuthroizationRequestRedirectFilter中保存在了AuthorizationRequestRepository中并且传递到了授权端，授权端再给客户端回传授权码时需要携带该参数
		// 如何避免CSRF攻击的呢？CSRF攻击一般是基于cookie认证的基础上进行的，通过设置恶意的符合cookie匹配路径的网址诱导用户去点击，从而将cookie信息携带到了后端
		// 执行了一些用户不知道的操作。那现在通过生成随机数，后端session中保存一份，然后传递给前端一份，前端的请求需要携带该随机数和session中的进行比对，
		// 那么黑客在构造恶意网址时就无法增加该参数，从而避免了CSRF攻击。
		// 那有的人就会问，再后端传递给前端时拦截获取该随机数，拼接到恶意网址后面不同样会造成攻击吗，是的，但是该攻击就不再属于CSRF
		// 攻击而是其他的攻击范围，会有其他的防范手段。 就好比登录时会将用户名密码传递后端校验，如果拦截了该请求那岂不是获取到了用户名密码，还攻击各个啥，光明正大的去搞破坏
		OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository
			.removeAuthorizationRequest(request, response);
		// 如果返回null，说明该请求不符合条件，session中不存在authorizationRequest对象或者state属性值比对不成功
		if (authorizationRequest == null) {
			OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}
		// 获取registration_id
		String registrationId = authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID);
		// 根据registration_id获取客户端配置信息
		ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
		if (clientRegistration == null) {
			OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE,
					"Client Registration not found with Id: " + registrationId, null);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}
		// @formatter:off
		// 获取request中的url 形如：http://127.0.0.1:8081/clientProject/login/oauth2/code/clientService
		String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
				.replaceQuery(null)
				.build()
				.toUriString();
		// @formatter:on
		// 以上都是之前的一些步骤的结果，将这些结果汇总成一个OAuth2AuthorizationResponse
		OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params,
				redirectUri);
		//返回一个WebAuthenticationDetails对象，里面封装了ip地址和sessionId
		Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);
		// 生成待认证的Authentication(之所以待认证，是因为此时该对象中的token，用户等属性信息暂时为null)
		// OAuth2LoginAuthenticationToken是Authentication接口实现类 (authorizationRequest为存储在session中的授权请求信息)
		OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration,
				new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
		authenticationRequest.setDetails(authenticationDetails);
		// 拿着code通过restTemplate的方式远程调用授权服务器获取token信息，注意不是重定向
		// 普通授权这里的AuthenticationManager实现类ProviderManager中起作用的AuthenticationProvider的子类OAuth2LoginAuthenticationProvider
		// oidc认证时使用的OidcAuthorizationCodeAuthenticationProvider 此时返回的OAuth2LoginAuthenticationToken中就已经填充了token，用户信息等
		// 再接下来的流程又创建了两个对象分别是OAuth2AuthenticationToken和OAuth2AuthorizedClient，这两个对象分摊了OAuth2LoginAuthenticationToken对象中的属性信息
		OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this
			.getAuthenticationManager()
			.authenticate(authenticationRequest);
		// 将OAuth2LoginAuthenticationToken转化为OAuth2AuthenticationToken
		// 注意：转化后的OAuth2AuthenticationToken就不含有授权服务器发放的accessToken和refreshToken了
		// 后续SecurityContext持有的和AuthenticaitonSuccessHandler中传入的都是OAuth2AuthenticationToken
		// 如果要保存accessToken和refreshToken，将在下面的this.authorizedClientRepository中保存
		OAuth2AuthenticationToken oauth2Authentication = this.authenticationResultConverter
			.convert(authenticationResult);
		Assert.notNull(oauth2Authentication, "authentication result cannot be null");
		// 将WebAuthenticationDetails对象设置到details属性中
		oauth2Authentication.setDetails(authenticationDetails);
		// 将token，客户端等信息封装为OAuth2AuthorizedClient对象，该对象意味着：客户端被认为是“已授权的”;当最终用户(资源所有者)具有授予客户端访问其受保护资源的授权
		OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
				authenticationResult.getClientRegistration(), oauth2Authentication.getName(),
				authenticationResult.getAccessToken(), authenticationResult.getRefreshToken());
		// 记录被认证的用户
		// OAuth2AuthorizedClientRepository默认实现为AuthenticatedPrincipalOAuth2AuthorizedClientRepository
		this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
		return oauth2Authentication;
	}

	/**
	 * Sets the repository for stored {@link OAuth2AuthorizationRequest}'s.
	 * @param authorizationRequestRepository the repository for stored
	 * {@link OAuth2AuthorizationRequest}'s
	 */
	public final void setAuthorizationRequestRepository(
			AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository) {
		Assert.notNull(authorizationRequestRepository, "authorizationRequestRepository cannot be null");
		this.authorizationRequestRepository = authorizationRequestRepository;
	}

	/**
	 * Sets the converter responsible for converting from
	 * {@link OAuth2LoginAuthenticationToken} to {@link OAuth2AuthenticationToken}
	 * authentication result.
	 * @param authenticationResultConverter the converter for
	 * {@link OAuth2AuthenticationToken}'s
	 * @since 5.6
	 */
	public final void setAuthenticationResultConverter(
			Converter<OAuth2LoginAuthenticationToken, OAuth2AuthenticationToken> authenticationResultConverter) {
		Assert.notNull(authenticationResultConverter, "authenticationResultConverter cannot be null");
		this.authenticationResultConverter = authenticationResultConverter;
	}

	private OAuth2AuthenticationToken createAuthenticationResult(OAuth2LoginAuthenticationToken authenticationResult) {
		return new OAuth2AuthenticationToken(authenticationResult.getPrincipal(), authenticationResult.getAuthorities(),
				authenticationResult.getClientRegistration().getRegistrationId());
	}

}
